"use strict";

var C = require('./common.js'),
    path = require('path');

var WebModule = C.Base.extend({
    /**
     * Parent module
     * @memberof WebModule#
     * @type {WebModule}
     */
    parent: null,

    /**
     * Web module name
     * @memberof WebModule#
     * @type {string}
     */
    name: null,

    /**
     * Absolute path of this module
     * @memberof WebModule#
     * @type {string}
     */
    root: null,

    /**
     * Relative path of this module
     * @memberof WebModule#
     * @type {string}
     */
    path: null,

    /**
     * Load stack of this module
     * @memberof WebModule#
     * @type {string}
     */
    stack: null,

    /**
     * The base route of this module
     * @memberof WebModule#
     * @type {string}
     */
    route: null,

    /**
     * Module specified services
     * @memberof WebModule#
     * @type {object}
     */
    $: {},

    /**
     * A nodeframe web module object.
     * @constructs WebModule
     * @param {WebModule} parent
     * @param {string} name - The web module name.
     * @param {object} moduleParams - The web module's extra options defined in its parent's configuration.
     */
    constructor: function (parent, name, moduleParams) {
        var self = this,
            _config,
            _options = moduleParams && moduleParams.options;

        if (moduleParams) {
            moduleParams.route || C.throwRequired('[' + parent.stack + '].modules[' + name + '].route');
        }

        this.name = parent ? name : 'app';
        this.path = parent ? path.join('web_modules', name) : '';
        this.root = parent ? parent.toAbsPath(this.path) : path.dirname(process.argv[1]);
        this.stack = parent && parent.stack ? parent.stack + '->' + this.name : this.name;
        this.route = parent ? C.ensureLeftSlash(C.trimRightSlash(C.urlJoin(parent.route, moduleParams.route))) : '';
        this.parent = parent;
        this.depth = parent ? parent.depth + 1 : 0;
        this.consoleIndent = C.S(' ').times(this.depth * 2).s;

        if (parent) {
            this.app = parent.app;
        } else {
            this.app = this;
        }

        this.extend({
            loadConfig: function () {
                var bootstrapGlobals = {
                    root: self.root,
                    toAbsPath: self.toAbsPath,
                    route: function (p) { return p ? C.urlJoin(self.route, p) : self.route; },
                    etc: function (path) { return self.etc(path) },
                    H: C.H
                };

                // load bootstrap file
                var bootstrapPath = path.resolve(self.root, './bootstrap.js'),
                    bootstrapContent = C.fs.readFileSync(bootstrapPath, 'utf8');

                var config = C.requireFromString(bootstrapContent, bootstrapPath, module, bootstrapGlobals);
                if (moduleParams.bootstrap) C._.extend(config, moduleParams.bootstrap);

                self._setConfig(config);
            },

            _setConfig: function (config) {
                _config = config;
            },

            /**
             * Boostrapping the module.
             * @param {object} options - bootstrap options
             * @param {completionCallback} cb
             */
            bootstrap: function (options, cb) {
                self.loadConfig();

                // features
                var features = {
                    init: [],
                    service: [],
                    engine: [],
                    prepare: [],
                    module: [],
                    server: []
                };

                C._.each(_config, function (block, name) {
                    var feature = self.loadExtension('features', name);

                    if (!feature.type) {
                        throw new Error('Expects feature type!');
                    }

                    if (options) {
                        if (options.excludeFeatures) {
                            if (options.excludeFeatures.indexOf(name) !== -1) {
                                self.console('Skipped feature "' + name + '".');
                                return;
                            }
                        }

                        if (options.excludeFeatureGroups) {
                            if (options.excludeFeatureGroups.indexOf(feature.type) !== -1) {
                                self.console('Skipped feature group "' + feature.type + '".');
                                return;
                            }
                        }
                    }

                    features[feature.type].push({name: name, feature: feature, config: block});
                });

                function loadFeature(f, loadedCallback) {
                    self.console('Loading feature "' + f.name + '" ...');

                    f.feature.load(self, f.config, function (err) {
                        if (err) return loadedCallback(err);

                        self.console('Feature "' + f.name + '" is successfully loaded.');
                        loadedCallback();
                    });
                }

                function stepComplete(messageHead, stepCallback) {
                    return function (err) {
                        if (err) return stepCallback(err);

                        self.console(messageHead + ' is done.');
                        console.log();

                        stepCallback();
                    }
                }

                C.async.series([
                    function(cb1){
                        self.console('Initializing [' + self.stack + '] ...');
                        C.async.eachSeries(features.init, loadFeature, stepComplete('Initializing', cb1));
                    },
                    function(cb1){
                        self.console('Installing services for [' + self.stack + '] ...');
                        C.async.eachSeries(features.service, loadFeature, stepComplete('Services installation', cb1));
                    },
                    function(cb1){
                        self.console('Setting up app engine for [' + self.stack + '] ...');
                        C.async.eachSeries(features.engine, loadFeature, stepComplete('Engine setup', cb1));
                    },
                    function(cb1){
                        self.console('Preparing for bootstrapping [' + self.stack + '] ...');
                        C.async.eachSeries(features.prepare, loadFeature, stepComplete('Preparation', cb1));
                    },
                    function(cb1){
                        self.console('Bootstrapping modules of [' + self.stack + '] ...');
                        C.async.eachSeries(features.module, loadFeature, stepComplete('Bootstrapping', cb1));
                    },
                    function(cb1){
                        self.console('Starting app servers of [' + self.stack + '] ...');
                        C.async.eachSeries(features.server, loadFeature, stepComplete('Servers starting', cb1));
                    }
                ], cb);
            },

            /**
             * Load an extension
             * @memberof WebModule#
             * @param {string} category - The category, can be 'auth_modules', 'middlewares', 'router_modules'
             * @param {string} extension
             * @returns {*}
             */
            loadExtension: function (category, extension) {
                var extensionHandle = path.join(category, extension);
                if (self.extensions && self.extensions[extensionHandle]) {
                    return self.extensions[extensionHandle];
                }

                var extensionRelative = extensionHandle + '.js',
                    extensionJs = self.toAbsPath(extensionRelative);

                if (!C.fs.existsSync(extensionJs)) {

                    if (parent) return parent.loadExtension(category, extension);

                    extensionJs = './' + extensionRelative;
                }

                self.extensions || (self.extensions = {});
                return (self.extensions[extensionHandle] = require(extensionJs));
            },

            /**
             * Get an option item by a period-separated path.
             * @memberof WebModule#
             * @param {string} [path]
             * @param {boolean} [required=false]
             * @returns {*}
             */
            etc: function (path, required) {
                var val = path ? C.getValueByPath(_options, path) : _options;
                if (required && typeof val === 'undefined') {
                    throw new C.Error.ConfigError('[' + parent.stack + '].modules.[' + name + '].options.[' + path + ']', 'Missing required config item.');
                }
                return val;
            },

            /**
             * Get a bootstrap option by a period-separated path.
             * @memberof WebModule#
             * @param {string} [path]
             * @param {boolean} [required=false]
             * @returns {*}
             */
            config: function (path, required) {
                var val = path ? C.getValueByPath(_config, path) : _config;
                if (required && typeof val === 'undefined') {
                    throw new C.Error.ConfigError('[' + self.stack + '].[' + path + ']', 'Missing required config item.');
                }
                return val;
            },

            /**
             * @memberof WebModule#
             * @returns {string}
             */
            toAbsPath: function (relPath) {
                return path.join(self.root, relPath);
            },

            /**
             * @memberof WebModule#
             * @returns {string}
             */
            ensureAbsPath: function (relPath) {
                return relPath.charAt(0) === path.sep ? relPath : path.join(self.root, relPath);
            },

            /**
             * @memberof WebModule#
             * @returns {string}
             */
            toPublicPath: function (relPath) {
                return '/' + C.urlJoin(self.path, relPath);
            },

            /**
             * Get a chain of middlewares from the root
             * @memberof WebModule#
             * @returns {*}
             */
            getMiddlewaresChain: function () {
                var chain = parent ? parent.getMiddlewaresChain() : [];
                return self.middlewares ? chain.concat(self.middlewares) : chain;
            }
        });
    },

    /**
     * Load a function by filename.function string.
     * @memberof WebModule#
     * @param relPathWithAction
     * @returns {*}
     */
    loadAction: function (relPathWithAction) {
        var lpos = relPathWithAction.lastIndexOf('.');
        if (lpos === -1) {
            throw new C.Error.UsageError('No action is specified in the argument!');
        }

        var controllerPath = relPathWithAction.substr(0, lpos);
        var actionName = relPathWithAction.substr(lpos+1);

        var controller = require(this.toAbsPath(controllerPath + '.js'));
        return controller[actionName];
    },

    /**
     * Write a line of console log with indent.
     * @memberof WebModule#
     * @param {string} message - Log message
     */
    console: function (message) {
        console.log(this.consoleIndent + message);
    },

    /**
     * Write a system log.
     * @memberof WebModule#
     * @param {int} level - Log level, e.g., error, warn, info, verbose, debug
     * @param {string} message - Message
     * @param {*} meta - Any extra meta data
     */
    log: function (level, message, meta) {
        this.logger && (meta ? this.logger.log(level, message, meta) : this.logger.log(level, message));
    },

    /**
     * Register a service
     * @memberof WebModule#
     * @param {string} name
     * @param {object} service
     */
    registerService: function (name, service) {
        if (name in this.$) {
            throw new C.Error.SystemDesignError('Service ['+ name +'] already exists!');
        }

        this.$[name] = service;
    },

    /**
     * Get a service from web module hierarchy
     * @memberof WebModule#
     * @param name
     * @returns {*}
     */
    service: function (name) {
        if (name in this.$) {
            return this.$[name];
        }

        if (this.parent) {
            return this.parent.service(name);
        }

        throw new C.Error.UsageError('Service [' + name + '] not found!');
    }
});

module.exports = WebModule;

/**
 * Controller middleware
 *
 * @callback middleware
 * @param {Request} req - Express request object
 * @param {Response} res - Express response object
 * @param {middleware} next - Next middleware
 */